/*
 * Copyright 2012 Finbudget.com
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package finbudgettable.client.ui.grid.cell;

import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.dom.client.BrowserEvents;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;

/**
 * An editable cell. Click to edit, escape to cancel, return to commit.
 *
 * @param <V> cell value type
 * @param <E> value type while editing
 * @see com.google.gwt.cell.client.EditTextCell
 */
public abstract class EditableGridCell<V, E> extends AbstractEditableGridCell<V, EditableGridCell<V, E>.ViewData> {


    public class ViewData {

        private boolean isEditing;

        /**
         * Keep track of the storedValue currentValue at the start of the edit, which might be
         * the edited currentValue from the previous edit and NOT the actual currentValue.
         */
        private V storedValue;

        private E currentValue;

        private boolean validated = true;

        /**
         * Construct a new ViewData in editing mode.
         *
         * @param currentValue the currentValue to edit
         */
        public ViewData(V storedValue, E currentValue) {
            this.storedValue = storedValue;
            this.currentValue = currentValue;
            this.isEditing = true;
        }

        @Override
        public boolean equals(Object o) {
            if (o != null && o instanceof EditableGridCell.ViewData) {
                @SuppressWarnings("unchecked")
				ViewData vd = (ViewData) o;
                return equalsOrBothNull(storedValue, vd.storedValue)
                        && equalsOrBothNull(currentValue, vd.currentValue) && isEditing == vd.isEditing;
            }
            return false;
        }

        public V getStoredValue() {
            return storedValue;
        }

        public E getCurrentValue() {
            return currentValue;
        }

        @Override
        public int hashCode() {
            return (storedValue == null ? 0 : storedValue.hashCode()) + 
            		(currentValue == null ? 0 : currentValue.hashCode()) +
                    Boolean.valueOf(isEditing).hashCode() * 29;
        }

        public boolean isEditing() {
            return isEditing;
        }

        public void setEditing(boolean isEditing) {
            this.isEditing = isEditing;
        }

        public void setCurrentValue(E currentValue) {
            this.currentValue = currentValue;
        }

        private boolean equalsOrBothNull(Object o1, Object o2) {
            return (o1 == null) ? o2 == null : o1.equals(o2);
        }

        @Override
        public String toString() {
            return "ViewData{" +
                    "isEditing=" + isEditing +
                    ", storedValue='" + storedValue + '\'' +
                    ", currentValue='" + currentValue + '\'' +
                    '}';
        }

        public boolean isValidated() {
            return validated;
        }

        public void setValidated(boolean validated) {
            this.validated = validated;
        }

        public void setStoredValue(V value) {
            storedValue = value;
        }
    }

    /**
     * Null values are allowed
     */
    private final boolean nullable;

    public EditableGridCell(boolean nullable) {
        super(BrowserEvents.CLICK,
                BrowserEvents.KEYUP,
                BrowserEvents.KEYDOWN,
                BrowserEvents.KEYPRESS,
                BrowserEvents.BLUR);
        this.nullable = nullable;
    }

    protected abstract boolean validate(E value);

    @Override
    public boolean isEditing(Context context, Element parent, V value) {
        ViewData viewData = getViewData(context.getKey());
        return viewData != null && viewData.isEditing();
    }

    private static final int KEY_F2 = 113;

    @Override
    public void onBrowserEvent(Context context, Element parent, V value,
                               NativeEvent event, ValueUpdater<V> valueUpdater) {
        //GWT.log(event.getType());
        if (isReadonly()) {
            return;
        }
        ViewData viewData = getViewData(context.getKey());
        if (viewData != null && viewData.isEditing()) {
            editEvent(context, parent, viewData, event, valueUpdater);
        } else {
            String type = event.getType();
            int keyCode = event.getKeyCode();
            boolean enterPressed = BrowserEvents.KEYUP.equals(type)
                    && (keyCode == KeyCodes.KEY_ENTER || keyCode == KEY_F2);
            if (BrowserEvents.CLICK.equals(type) || enterPressed) {
                startEdit(context, parent, value, viewData);
            }
        }
    }

    protected void startEdit(Context context, Element parent, V value, ViewData viewData) {
        if (viewData == null) {
            viewData = new ViewData(value, convertValueToEditedValue(value));
            setViewData(context.getKey(), viewData);
        } else {
            viewData.setEditing(true);
        }
        edit(context, parent, viewData.getStoredValue());
    }

    protected abstract SafeHtml getEditorHtml(ViewData viewData);

    protected abstract SafeHtml getViewHtml(V value);

    protected abstract SafeHtml getErrorHtml(E value);


    @Override
    public void render(Context context, V value, SafeHtmlBuilder sb) {
        Object key = context.getKey();
        ViewData viewData = getViewData(key);
        if (viewData != null && !viewData.isEditing() && value != null
                && value.equals(viewData.getCurrentValue())) {
            clearViewData(key);
            viewData = null;
        }
        if (viewData != null) {
            if (viewData.isEditing()) {
                sb.append(getEditorHtml(viewData));
            } else {
                if (viewData.isValidated()) {
                    sb.append(getViewHtml(viewData.getStoredValue()));
                } else {
                    sb.append(getErrorHtml(viewData.getCurrentValue()));
                }
            }
        } else if (value != null) {
            sb.append(getViewHtml(value));
        } else {
            // Render null value
            // TODO
        }
    }

    @Override
    public boolean resetFocus(Context context, Element parent, V value) {
        if (isEditing(context, parent, value)) {
            setFocus(parent, true);
            return true;
        }
        return false;
    }

    protected abstract void setFocus(Element parent, boolean focused);

    /**
     * Convert the cell to edit mode.
     *
     * @param context the {@link com.google.gwt.cell.client.Cell.Context} of the cell
     * @param parent  the parent element
     * @param value   the current currentValue
     */
    protected void edit(Context context, Element parent, V value) {
        setValue(context, parent, value);
        setFocus(parent, true);
    }

    /**
     * Commit the currentValue.
     *
     * @param context      the context of the cell
     * @param parent       the parent Element
     * @param viewData     the {@link ViewData} object
     * @param valueUpdater the {@link com.google.gwt.cell.client.ValueUpdater}
     */
    protected void commit(Context context, Element parent, ViewData viewData,
                          ValueUpdater<V> valueUpdater) {
        updateViewData(parent, viewData, false);
        setValue(context, parent, viewData.getStoredValue());
        if (valueUpdater != null && viewData.isValidated()) {
            valueUpdater.update(viewData.getStoredValue());
        }
    }

    protected void editEvent(Context context, Element parent, ViewData viewData,
                             NativeEvent event, ValueUpdater<V> valueUpdater) {
        String type = event.getType();
        boolean keyUp = BrowserEvents.KEYUP.equals(type);
        boolean keyDown = BrowserEvents.KEYDOWN.equals(type);
        if (keyUp || keyDown) {
            int keyCode = event.getKeyCode();
            if (keyUp && keyCode == KeyCodes.KEY_ENTER) {
                commit(context, parent, viewData, valueUpdater);
                if (!viewData.isValidated()) {
                    showIncorrectValueError(context, parent, viewData);
                }
            } else if (keyUp && keyCode == KeyCodes.KEY_ESCAPE) {
                cancel(context, parent, viewData.getStoredValue(), viewData);
            } else {
                // Update the currentValue in the view data on each key.
                if (keyUp) {
                    updateViewData(parent, viewData, true);
                }
            }
        }
    }

    /**
     * Convert the cell to non-edit mode.
     *
     * @param context the context of the cell
     * @param parent  the parent Element
     * @param value   the currentValue associated with the cell
     */
    protected void cancel(Context context, Element parent, V value, ViewData viewData) {
        viewData.setEditing(false);
        viewData.setValidated(true);
        setViewData(context.getKey(), null);
        setValue(context, parent, value);
    }

    protected abstract E convertValueToEditedValue(V value);

    protected abstract V convertEditedValueToValue(E editedValue);

    private void updateViewData(Element parent, ViewData viewData, boolean isEditing) {
        E editedValue = getEditedValue(parent);
        viewData.setValidated(validate(editedValue));
        viewData.setCurrentValue(editedValue);
        viewData.setEditing(isEditing);
        if (!isEditing && viewData.isValidated()) {
            viewData.setStoredValue(convertEditedValueToValue(viewData.getCurrentValue()));
        }
    }

    protected abstract E getEditedValue(Element parent);

    public boolean isNullable() {
        return nullable;
    }

    protected abstract void showIncorrectValueError(Context context, Element parent, ViewData viewData);
}